<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8" />
  <title>股权穿透图</title>
</head>
<script src="./d3.v7.min.js"></script>
<style>
  html,
  body {
    margin: 0;
  }

  html,body {
    width: 100%;
    height: 100%;
    background-color: #e1e1e1;
  }

  #app {
    width: 1600px;
    height: 800px;
    overflow: hidden;
    box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.1);
    box-sizing: border-box;
    background-color: #ffffff;
    border-radius: 8px;
    position: relative;
    margin: 0 auto;
  }
</style>
<body>
<div id="app"></div>
</body>
<script>
  let data = {
    id: "abc1005",
    // 根节点名称
    name: "山东吠舍科技有限责任公司",
    // 子节点列表
    children: [
      {
        id:"abc1006",
        name: "山东第一首陀罗科技服务有限公司",
        percent: '100%',
      },
      {
        id:"abc1007",
        name: "山东第二首陀罗程技术有限公司",
        percent: '100%',
      },
      {
        id:"abc1008",
        name: "山东第三首陀罗光伏材料有限公司",
        percent: '100%',
      },
      {
        id:"abc1009",
        name: "山东第四首陀罗科技发展有限公司",
        percent: '100%',
        children: [
          {
            id:"abc1010",
            name: "山东第一达利特瑞利分析仪器有限公司",
            percent: '100%',
            children:[
              {
                id:"abc1011",
                name: "山东瑞利的子公司一",
                percent: '80%',
              },
              {
                id:"abc1012",
                name: "山东瑞利的子公司二",
                percent: '90%',
              },
              {
                id:"abc1013",
                name: "山东瑞利的子公司三",
                percent: '100%',
              },
            ]
          }
        ]
      },
      {
        id:"abc1014",
        name: "山东第五首陀罗电工科技有限公司",
        percent: '100%',
        children: [
          {
            id:"abc1015",
            name: "山东第二达利特低自动化设备有限公司",
            percent: '100%',
            children:[
              {
                id:"abc1016",
                name: "山东敬业的子公司一",
                percent: '100%',
              },
              {
                id:"abc1017",
                name: "山东敬业的子公司二",
                percent: '90%',
              }
            ]
          }
        ]
      },
      {
        id: "abc1020",
        name: "山东第六首陀罗分析仪器(集团)有限责任公司",
        percent: '100%',
        children: [
          {
            id:"abc1021",
            name: "山东第三达利特分气体工业有限公司",
          }
        ]
      },
    ],
    // 父节点列表
    parents: [
      {
        id: "abc2001",
        name: "山东刹帝利集团有限责任公司",
        percent: '60%',
        parents: [
          {
            id: "abc2000",
            name: "山东婆罗门集团有限公司",
            percent: '100%',
          },
        ]
      },
      {
        id: "abc2002",
        name: "吴小远",
        percent: '40%',
      }
    ],
  }
</script>
<script>
  // 股权树
  class StockTree {
    constructor(options) {
      // 树的源数据
      this.originTreeData = options.originTreeData;
      // 宿主元素选择器
      this.el = options.el;
      this.nodeClickEvent = options.nodeClickEvent || function (e,d){
        alert(d.name)
      }
      // 一些配置项
      this.config = {
        // 节点的横向距离
        dx: 200,
        // 节点的纵向距离
        dy: 170,
        // svg的viewBox的宽度
        width: 0,
        // svg的viewBox的高度
        height: 0,
        // 节点的矩形框宽度
        rectWidth: 170,
        // 节点的矩形框高度
        rectHeight: 70,
      };
      this.svg = null;
      this.gAll = null;
      this.gLinks = null;
      this.gNodes = null;
      // 给树加坐标点的方法
      this.tree = null;
      // 投资公司树的根节点
      this.rootOfDown = null;
      // 股东树的根节点
      this.rootOfUp = null;

      this.drawChart({
        type: 'fold',
      });
    }

    // 初始化树结构数据
    drawChart(options) {
      // 宿主元素的d3选择器对象
      let host = d3.select(this.el);
      // 宿主元素的DOM，通过node()获取到其DOM元素对象
      let dom = host.node();
      // 宿主元素的DOMRect
      let domRect = dom.getBoundingClientRect();
      // svg的宽度和高度
      this.app.width = domRect.width;
      this.app.height = domRect.height;

      let oldSvg = d3.select('svg')
      // 如果宿主元素中包含svg标签了，那么则删除这个标签，再重新生成一个
      if(!oldSvg.empty()){
        oldSvg.remove();
      }

      const svg = d3
              .create("svg")
              .attr("viewBox", () => {
                let parentsLength = this.originTreeData.parents ? this.originTreeData.parents.length : 0;
                return [
                  -this.app.width / 2,
                  // 如果有父节点，则根节点居中，否则根节点上浮一段距离
                  parentsLength > 0 ? -this.app.height / 2 : -this.app.height / 3,
                  this.app.width,
                  this.app.height,
                ]
              })
              .style("user-select", "none")
              .style("cursor","move");

      // 包括连接线和节点的总集合
      const gAll = svg.append("g").attr("id", "all");
      svg.call(
              d3
                      .zoom()
                      .scaleExtent([0.2, 5])
                      .on("zoom", (e) => {
                        gAll.attr("transform", () => {
                          return `translate(${e.transform.x},${e.transform.y}) scale(${e.transform.k})`;
                        });
                      })
      ).on("dblclick.zoom", null);// 取消默认的双击放大事件

      this.gAll = gAll;
      // 连接线集合
      this.gLinks = gAll.append("g").attr("id", "linkGroup");
      // 节点集合
      this.gNodes = gAll.append("g").attr("id", "nodeGroup");
      // 设置好节点之间距离的tree方法
      this.tree = d3.tree().nodeSize([this.app.dx, this.app.dy]);

      this.rootOfDown = d3.hierarchy(this.originTreeData,(d) => d.children);
      this.rootOfUp = d3.hierarchy(this.originTreeData,(d) => d.parents);
      this.tree(this.rootOfDown);

      [this.rootOfDown.descendants(), this.rootOfUp.descendants()].forEach((nodes) => {
        nodes.forEach((node) => {
          node._children = node.children || null;
          if(options.type === 'all'){
            //如果是all的话，则表示全部都展开
            node.children = node._children;
          } else if(options.type === 'fold'){ //如果是fold则表示除了父节点全都折叠
            // 将非根节点的节点都隐藏掉（其实对于这个组件来说加不加都一样）
            if (node.depth) {
              node.children = null;
            }
          }
        });
      });

      //箭头(下半部分)
      svg
              .append("marker")
              .attr("id", "markerOfDown")
              .attr("markerUnits", "userSpaceOnUse")
              .attr("viewBox", "0 -5 10 10") //坐标系的区域
              .attr("refX", 55) //箭头坐标
              .attr("refY", 0)
              .attr("markerWidth", 10) //标识的大小
              .attr("markerHeight", 10)
              .attr("orient", "90") //绘制方向，可设定为：auto（自动确认方向）和 角度值
              .attr("stroke-width", 2) //箭头宽度
              .append("path")
              .attr("d", "M0,-5L10,0L0,5") //箭头的路径
              .attr("fill", "#215af3"); //箭头颜色

      //箭头(上半部分)
      svg
              .append("marker")
              .attr("id", "markerOfUp")
              .attr("markerUnits", "userSpaceOnUse")
              .attr("viewBox", "0 -5 10 10") //坐标系的区域
              .attr("refX", -50) //箭头坐标
              .attr("refY", 0)
              .attr("markerWidth", 10) //标识的大小
              .attr("markerHeight", 10)
              .attr("orient", "90") //绘制方向，可设定为：auto（自动确认方向）和 角度值
              .attr("stroke-width", 2) //箭头宽度
              .append("path")
              .attr("d", "M0,-5L10,0L0,5") //箭头的路径
              .attr("fill", "#215af3"); //箭头颜色

      this.svg = svg;
      this.update();
      // 将svg置入宿主元素中
      host.append(function () {
        return svg.node();
      });
    }

    // 更新数据
    update(source) {
      if (!source) {
        source = {
          x0: 0,
          y0: 0,
        };
        // 设置根节点所在的位置（原点）
        this.rootOfDown.x0 = 0;
        this.rootOfDown.y0 = 0;
        this.rootOfUp.x0 = 0;
        this.rootOfUp.y0 = 0;
      }

      let nodesOfDown = this.rootOfDown.descendants().reverse();
      let linksOfDown = this.rootOfDown.links();
      let nodesOfUp = this.rootOfUp.descendants().reverse();
      let linksOfUp = this.rootOfUp.links();

      this.tree(this.rootOfDown);
      this.tree(this.rootOfUp);

      const myTransition = this.svg.transition().duration(500);

      /***  绘制子公司树  ***/
      const node1 = this.gNodes
              .selectAll("g.nodeOfDownItemGroup")
              .data(nodesOfDown, (d) => {
                return d.data.id;
              });

      const node1Enter = node1
              .enter()
              .append("g")
              .attr("class", "nodeOfDownItemGroup")
              .attr("transform", (d) => {
                return `translate(${source.x0},${source.y0})`;
              })
              .attr("fill-opacity", 0)
              .attr("stroke-opacity", 0)
              .style("cursor", "pointer");

      // 外层的矩形框
      node1Enter
              .append("rect")
              .attr("width", (d) => {
                if (d.depth === 0) {
                  return (d.data.name.length + 2) * 16;
                }
                return this.app.rectWidth;
              })
              .attr("height", (d) => {
                if (d.depth === 0) {
                  return 30;
                }
                return this.app.rectHeight;
              })
              .attr("x", (d) => {
                if (d.depth === 0) {
                  return (-(d.data.name.length + 2) * 16) / 2;
                }
                return -this.app.rectWidth / 2;
              })
              .attr("y", (d) => {
                if (d.depth === 0) {
                  return -15;
                }
                return -this.app.rectHeight / 2;
              })
              .attr("rx", 5)
              .attr("stroke-width", 1)
              .attr("stroke", (d) => {
                if (d.depth === 0) {
                  return "#5682ec";
                }
                return "#7A9EFF";
              })
              .attr("fill", (d) => {
                if (d.depth === 0) {
                  return "#7A9EFF";
                }
                return "#FFFFFF";
              })
              .on("click", (e, d) => {
                this.nodeClickEvent(e,d)
              });
      // 文本主标题
      node1Enter
              .append("text")
              .attr("class", "main-title")
              .attr("x", (d) => {
                return 0;
              })
              .attr("y", (d) => {
                if (d.depth === 0) {
                  return 5;
                }
                return -14;
              })
              .attr("text-anchor", (d) => {
                return "middle";
              })
              .text((d) => {
                if (d.depth === 0) {
                  return d.data.name;
                } else {
                  return d.data.name.length > 11
                          ? d.data.name.substring(0, 11)
                          : d.data.name;
                }
              })
              .attr("fill", (d) => {
                if (d.depth === 0) {
                  return "#FFFFFF";
                }
                return "#000000";
              })
              .style("font-size", (d) => (d.depth === 0 ? 16 : 14))
              .style('font-family','黑体')
              .style("font-weight", "bold");
      // 副标题
      node1Enter
              .append("text")
              .attr("class", "sub-title")
              .attr("x", (d) => {
                return 0;
              })
              .attr("y", (d) => {
                return 5;
              })
              .attr("text-anchor", (d) => {
                return "middle";
              })
              .text((d) => {
                if (d.depth !== 0) {
                  let subTitle = d.data.name.substring(11);
                  if (subTitle.length > 10) {
                    return subTitle.substring(0, 10) + "...";
                  }
                  return subTitle;
                }
              })
              .style("font-size", (d) => 14)
              .style('font-family','黑体')
              .style("font-weight", "bold");

      // 控股比例
      node1Enter
              .append("text")
              .attr("class", "percent")
              .attr("x", (d) => {
                return 12;
              })
              .attr("y", (d) => {
                return -45;
              })
              .text((d) => {
                if (d.depth !== 0) {
                  return d.data.percent;
                }
              })
              .attr("fill", "#000000")
              .style('font-family','黑体')
              .style("font-size", (d) => 14);

      // 增加展开按钮
      const expandBtnG = node1Enter
              .append("g")
              .attr("class", "expandBtn")
              .attr("transform", (d) => {
                return `translate(${0},${this.app.rectHeight / 2})`;
              })
              .style("display", (d) => {
                // 如果是根节点，不显示
                if (d.depth === 0) {
                  return "none";
                }
                // 如果没有子节点，则不显示
                if (!d._children) {
                  return "none";
                }
              })
              .on("click", (e, d) => {
                if (d.children) {
                  d._children = d.children;
                  d.children = null;
                } else {
                  d.children = d._children;
                }
                this.update(d);
              });

      expandBtnG
              .append("circle")
              .attr("r", 8)
              .attr("fill", "#7A9EFF")
              .attr("cy", 8);

      expandBtnG
              .append("text")
              .attr("text-anchor", "middle")
              .attr("fill", "#ffffff")
              .attr("y", 13)
              .style('font-size', 16)
              .style('font-family','微软雅黑')
              .text((d)=>{
                return d.children ? "-" : "+"
              });

      const link1 = this.gLinks
              .selectAll("path.linkOfDownItem")
              .data(linksOfDown, (d) => d.target.data.id);

      const link1Enter = link1
              .enter()
              .append("path")
              .attr("class", "linkOfDownItem")
              .attr("d", (d) => {
                let o = {
                  source: {
                    x: source.x0,
                    y: source.y0,
                  },
                  target: {
                    x: source.x0,
                    y: source.y0,
                  },
                };
                return this.drawLink(o);
              })
              .attr("fill", "none")
              .attr("stroke", "#7A9EFF")
              .attr("stroke-width", 1)
              .attr("marker-end", "url(#markerOfDown)");

      // 有元素update更新和元素新增enter的时候
      node1
              .merge(node1Enter)
              .transition(myTransition)
              .attr("transform", (d) => {
                return `translate(${d.x},${d.y})`;
              })
              .attr("fill-opacity", 1)
              .attr("stroke-opacity", 1);

      // 有元素消失时
      node1
              .exit()
              .transition(myTransition)
              .remove()
              .attr("transform", (d) => {
                return `translate(${source.x0},${source.y0})`;
              })
              .attr("fill-opacity", 0)
              .attr("stroke-opacity", 0);

      link1.merge(link1Enter).transition(myTransition).attr("d", this.drawLink);

      link1
              .exit()
              .transition(myTransition)
              .remove()
              .attr("d", (d) => {
                let o = {
                  source: {
                    x: source.x,
                    y: source.y,
                  },
                  target: {
                    x: source.x,
                    y: source.y,
                  },
                };
                return this.drawLink(o);
              });

      /***  绘制股东树  ***/

      nodesOfUp.forEach(node => {
        node.y = -node.y
      })

      const node2 = this.gNodes
              .selectAll("g.nodeOfUpItemGroup")
              .data(nodesOfUp, (d) => {
                return d.data.id;
              });

      const node2Enter = node2
              .enter()
              .append("g")
              .attr("class", "nodeOfUpItemGroup")
              .attr("transform", (d) => {
                return `translate(${source.x0},${source.y0})`;
              })
              .attr("fill-opacity", 0)
              .attr("stroke-opacity", 0)
              .style("cursor", "pointer");

      // 外层的矩形框
      node2Enter
              .append("rect")
              .attr("width", (d) => {
                if (d.depth === 0) {
                  return (d.data.name.length + 2) * 16;
                }
                return this.app.rectWidth;
              })
              .attr("height", (d) => {
                if (d.depth === 0) {
                  return 30;
                }
                return this.app.rectHeight;
              })
              .attr("x", (d) => {
                if (d.depth === 0) {
                  return (-(d.data.name.length + 2) * 16) / 2;
                }
                return -this.app.rectWidth / 2;
              })
              .attr("y", (d) => {
                if (d.depth === 0) {
                  return -15;
                }
                return -this.app.rectHeight / 2;
              })
              .attr("rx", 5)
              .attr("stroke-width", 1)
              .attr("stroke", (d) => {
                if (d.depth === 0) {
                  return "#5682ec";
                }
                return "#7A9EFF";
              })
              .attr("fill", (d) => {
                if (d.depth === 0) {
                  return "#7A9EFF";
                }
                return "#FFFFFF";
              })
              .on("click", (e, d) => {
                this.nodeClickEvent(e,d)
              });
      // 文本主标题
      node2Enter
              .append("text")
              .attr("class", "main-title")
              .attr("x", (d) => {
                return 0;
              })
              .attr("y", (d) => {
                if (d.depth === 0) {
                  return 5;
                }
                return -14;
              })
              .attr("text-anchor", (d) => {
                return "middle";
              })
              .text((d) => {
                if (d.depth === 0) {
                  return d.data.name;
                } else {
                  return d.data.name.length > 11
                          ? d.data.name.substring(0, 11)
                          : d.data.name;
                }
              })
              .attr("fill", (d) => {
                if (d.depth === 0) {
                  return "#FFFFFF";
                }
                return "#000000";
              })
              .style("font-size", (d) => (d.depth === 0 ? 16 : 14))
              .style('font-family','黑体')
              .style("font-weight", "bold");
      // 副标题
      node2Enter
              .append("text")
              .attr("class", "sub-title")
              .attr("x", (d) => {
                return 0;
              })
              .attr("y", (d) => {
                return 5;
              })
              .attr("text-anchor", (d) => {
                return "middle";
              })
              .text((d) => {
                if (d.depth !== 0) {
                  let subTitle = d.data.name.substring(11);
                  if (subTitle.length > 10) {
                    return subTitle.substring(0, 10) + "...";
                  }
                  return subTitle;
                }
              })
              .style("font-size", (d) => 14)
              .style('font-family','黑体')
              .style("font-weight", "bold");

      // 控股比例
      node2Enter
              .append("text")
              .attr("class", "percent")
              .attr("x", (d) => {
                return 12;
              })
              .attr("y", (d) => {
                return 55;
              })
              .text((d) => {
                if (d.depth !== 0) {
                  return d.data.percent;
                }
              })
              .attr("fill", "#000000")
              .style('font-family','黑体')
              .style("font-size", (d) => 14);

      // 增加展开按钮
      const expandBtnG2 = node2Enter
              .append("g")
              .attr("class", "expandBtn")
              .attr("transform", (d) => {
                return `translate(${0},${-this.app.rectHeight / 2})`;
              })
              .style("display", (d) => {
                // 如果是根节点，不显示
                if (d.depth === 0) {
                  return "none";
                }
                // 如果没有子节点，则不显示
                if (!d._children) {
                  return "none";
                }
              })
              .on("click", (e, d) => {
                if (d.children) {
                  d._children = d.children;
                  d.children = null;
                } else {
                  d.children = d._children;
                }
                this.update(d);
              });

      expandBtnG2
              .append("circle")
              .attr("r", 8)
              .attr("fill", "#7A9EFF")
              .attr("cy", -8);

      expandBtnG2
              .append("text")
              .attr("text-anchor", "middle")
              .attr("fill", "#ffffff")
              .attr("y", -3)
              .style('font-size', 16)
              .style('font-family','微软雅黑')
              .text((d)=>{
                return d.children ? "-" : "+"
              });

      const link2 = this.gLinks
              .selectAll("path.linkOfUpItem")
              .data(linksOfUp, (d) => d.target.data.id);

      const link2Enter = link2
              .enter()
              .append("path")
              .attr("class", "linkOfUpItem")
              .attr("d", (d) => {
                let o = {
                  source: {
                    x: source.x0,
                    y: source.y0,
                  },
                  target: {
                    x: source.x0,
                    y: source.y0,
                  },
                };
                return this.drawLink(o);
              })
              .attr("fill", "none")
              .attr("stroke", "#7A9EFF")
              .attr("stroke-width", 1)
              .attr("marker-end", "url(#markerOfUp)");

      // 有元素update更新和元素新增enter的时候
      node2
              .merge(node2Enter)
              .transition(myTransition)
              .attr("transform", (d) => {
                return `translate(${d.x},${d.y})`;
              })
              .attr("fill-opacity", 1)
              .attr("stroke-opacity", 1);

      // 有元素消失时
      node2
              .exit()
              .transition(myTransition)
              .remove()
              .attr("transform", (d) => {
                return `translate(${source.x0},${source.y0})`;
              })
              .attr("fill-opacity", 0)
              .attr("stroke-opacity", 0);

      link2.merge(link2Enter).transition(myTransition).attr("d", this.drawLink);

      link2
              .exit()
              .transition(myTransition)
              .remove()
              .attr("d", (d) => {
                let o = {
                  source: {
                    x: source.x,
                    y: source.y,
                  },
                  target: {
                    x: source.x,
                    y: source.y,
                  },
                };
                return this.drawLink(o);
              });

      // node数据改变的时候更改一下加减号
      const expandButtonsSelection = d3.selectAll('g.expandBtn')

      expandButtonsSelection.select('text').transition().text((d) =>{
        return d.children ? "-" : "+";
      })

      this.rootOfDown.eachBefore((d) => {
        d.x0 = d.x;
        d.y0 = d.y;
      });
      this.rootOfUp.eachBefore((d) => {
        d.x0 = d.x;
        d.y0 = d.y;
      });
    }

    // 直角连接线 by wushengyuan
    drawLink({ source, target }) {
      const halfDistance = (target.y - source.y) / 2;
      const halfY = source.y + halfDistance;
      return `M${source.x},${source.y} L${source.x},${halfY} ${target.x},${halfY} ${target.x},${target.y}`;
    }

    // 展开所有的节点
    expandAllNodes(){
      this.drawChart({
        type: 'all',
      })
    }

    // 将所有节点都折叠
    foldAllNodes(){
      this.drawChart({
        type: 'fold',
      })
    }
  }

  const tree = new StockTree({
    el: "#app",
    originTreeData: data,
      // 节点点击事件
    nodeClickEvent: function (e,d){
      console.log('当前节点的数据：',d)
    }
  });
</script>
</html>
